<?php
/**
 * Product: ASW.Communication.
 * Date: 2024-05-16
 * Time: 09:14
 */

namespace ASW\Communication;


use ASW\Communication\Rdc\RdcClientRequest;
use ASW\Communication\Rdc\RdcServerCommand;
use ASW\Communication\Rdc\RdcServerResponse;
use ASW\Utility\ExecuteResult;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;

class EventChannelServer
{
    private Worker $server;
    private string $bindHost;
    private array  $eventConnectionsDic = [];
    public string  $serverName          = 'EventChannelServer';

    public int $serverCount = 1;

    /**
     * @var callable $onServerStart
     */
    public $onServerStart = null;

    /**
     * @var callable $onClientSendBufferFull
     */
    public $onClientSendBufferFull = null;

    /**
     * @var callable $onClientSendBufferDrain
     */
    public $onClientSendBufferDrain = null;

    /**
     * @var callable $onClientConnect
     */
    public $onClientConnect = null;

    /**
     * @var callable $onClientClose
     */
    public $onClientClose = null;

    /**
     * @var callable $onClientError
     */
    public $onClientError = null;

    /**
     * @var callable $onClientRequest
     */
    public $onClientRequest = null;

    /**
     * @var callable $onClientSubscribe
     */
    public $onClientSubscribe = null;

    /**
     * @var callable $onClientUnSubscribe
     */
    public $onClientUnSubscribe = null;

    /**
     * @var callable $onClientPublish
     */
    public $onClientPublish = null;

    /**
     * @var callable $onServerPublishCommandCreate 服务端已准备好下发给客户端的事件通知, 可以在此处修改要下发的 command 内容
     */
    public $onServerPublishCommandCreate = null;

    /**
     * @var callable $onClientEventDiscard 事件被丢弃
     */
    public $onClientEventDiscard = null;

    /**
     * @var callable $onBeforeSendResponse
     */
    public $onBeforeSendResponse = null;

    /**
     * @var callable $onBeforeSendCommand
     */
    public $onBeforeSendCommand = null;

    /**
     * @var callable $onAfterSendResponse
     */
    public $onAfterSendResponse = null;

    /**
     * @var callable $onAfterSendCommand
     */
    public $onAfterSendCommand = null;

    public function __construct(string $bindHost = '0.0.0.0:2018')
    {
        $this->bindHost = $bindHost;
    }

    public function start(): void
    {
        $this->server       = new Worker("rdcs://$this->bindHost");
        $this->server->name = $this->serverName;

        if ($this->serverCount <= 0) throw new \Exception("server count must larger then 0, current is $this->serverCount");
        $this->server->count = $this->serverCount;

        $this->server->onWorkerStart = function () {
            if (is_callable($this->onServerStart)) {
                call_user_func($this->onServerStart);
            }
        };

        $this->server->onBufferFull = function (TcpConnection $connection) {
            $connection->bufferIsFull = true;
            if (is_callable($this->onClientSendBufferFull)) {
                call_user_func($this->onClientSendBufferFull, $connection);
            }
        };

        $this->server->onBufferDrain = function (TcpConnection $connection) {
            $connection->bufferIsFull = false;
            if (is_callable($this->onClientSendBufferDrain)) {
                call_user_func($this->onClientSendBufferDrain, $connection);
            }
        };

        $this->server->onConnect = function (TcpConnection $connection) {
            $connection->bufferIsFull = false;
            $this->handleClientConnect($connection);
        };

        $this->server->onClose = function (TcpConnection $connection) {
            $this->handleClientClose($connection);
        };

        $this->server->onError = function (TcpConnection $connection, int $code, string $msg) {
            $this->handleClientError($connection, $code, $msg);
        };

        $this->server->onMessage = function (TcpConnection $connection, RdcClientRequest $request) {
            $this->handleClientRequest($connection, $request);
        };
    }

    private function handleClientConnect(TcpConnection $connection): void
    {
        if (is_callable($this->onClientConnect)) {
            call_user_func($this->onClientConnect, $connection);
        }
    }

    private function handleClientClose(TcpConnection $connection): void
    {
        $eventNames = array_keys($this->eventConnectionsDic);
        foreach ($eventNames as $eventName) {
            unset($this->eventConnectionsDic[$eventName][$connection->id]);
        }

        if (is_callable($this->onClientClose)) {
            call_user_func($this->onClientClose, $connection);
        }
    }

    private function handleClientError(TcpConnection $connection, int $code, string $msg): void
    {
        if (is_callable($this->onClientError)) {
            call_user_func($this->onClientError, $connection, $code, $msg);
        }
    }

    private function handleClientRequest(TcpConnection $connection, RdcClientRequest $request): void
    {
        if (is_callable($this->onClientRequest)) {
            call_user_func($this->onClientRequest, $connection, $request);
        }

        if (str_starts_with($request->action, 'ping')) {
            $this->sendResponse($connection, RdcServerResponse::fromRequest($request, ExecuteResult::success()));
            return;
        }

        if ($request->action === 'command_reply') {
            $this->sendResponse($connection, RdcServerResponse::fromRequest($request, ExecuteResult::success()));
            return;
        }

        $eventName = $request->args['eventName'] ?? null;
        if ($eventName === null) {
            $this->handleClientError($connection, -400, 'lack of args eventName');
            $connection->close();
            return;
        }

        if (empty($eventName)) {
            $this->handleClientError($connection, -400, 'eventName is empty');
            $connection->close();
            return;
        }

        if ($request->action === 'subscribe') {
            $this->handleClientSubscribe($connection, $eventName);
        } else if ($request->action === 'unsubscribe') {
            $this->handleClientUnSubscribe($connection, $eventName);
        } else if ($request->action === 'publish') {
            $eventArgs = $request->args['eventArgs'];
            $this->handleClientPublish($connection, $eventName, $eventArgs);
        }
        $this->sendResponse($connection, RdcServerResponse::fromRequest($request, ExecuteResult::success()));
    }

    private function handleClientPublish(TcpConnection $connection, string $eventName, array $eventArgs): void
    {
        if (is_callable($this->onClientPublish)) {
            call_user_func($this->onClientPublish, $connection, $eventName, $eventArgs);
        }

        if (array_key_exists($eventName, $this->eventConnectionsDic)) {
            $command = RdcServerCommand::create('publish', [
                'eventName' => $eventName,
                'eventArgs' => $eventArgs
            ]);

            if (is_callable($this->onServerPublishCommandCreate)) {
                call_user_func($this->onServerPublishCommandCreate, $command);
            }

            foreach ($this->eventConnectionsDic[$eventName] as $connectionId => $connection) {
                if ($connection === null) continue;
                if ($connection->bufferIsFull === true) {
                    // discard event because connection send buffer is full
                    if (is_callable($this->onClientEventDiscard)) {
                        call_user_func($this->onClientEventDiscard, $connection, $command, 'connection send buffer is full');
                    }
                    continue;
                }
                $this->sendCommand($connection, $command);
            }
        }
    }

    private function handleClientSubscribe(TcpConnection $connection, string $eventName): void
    {
        $connectionId = $connection->id;

        if (!array_key_exists($eventName, $this->eventConnectionsDic)) {
            $this->eventConnectionsDic[$eventName] = [];
        }
        $this->eventConnectionsDic[$eventName][$connectionId] = $connection;

        if (is_callable($this->onClientSubscribe)) {
            call_user_func($this->onClientSubscribe, $connection, $eventName);
        }
    }

    private function handleClientUnSubscribe(TcpConnection $connection, string $eventName): void
    {
        $connectionId = $connection->id;

        if (array_key_exists($eventName, $this->eventConnectionsDic)) {
            unset($this->eventConnectionsDic[$eventName][$connectionId]);
        }

        if (is_callable($this->onClientUnSubscribe)) {
            call_user_func($this->onClientUnSubscribe, $connection, $eventName);
        }
    }

    private function sendResponse(TcpConnection $connection, RdcServerResponse $response): void
    {
        if (is_callable($this->onBeforeSendResponse)) {
            call_user_func($this->onBeforeSendResponse, $connection, $response);
        }

        $sendResult = $connection->send($response);

        if (is_callable($this->onAfterSendResponse)) {
            call_user_func($this->onAfterSendResponse, $connection, $response, $sendResult);
        }
    }

    private function sendCommand(TcpConnection $connection, RdcServerCommand $command): void
    {
        if (is_callable($this->onBeforeSendCommand)) {
            call_user_func($this->onBeforeSendCommand, $connection, $command);
        }

        $sendResult = $connection->send($command);

        if (is_callable($this->onAfterSendCommand)) {
            call_user_func($this->onAfterSendCommand, $connection, $command, $sendResult);
        }
    }

    public function getEventSubscriberCount(string $eventName): int
    {
        if (!array_key_exists($eventName, $this->eventConnectionsDic)) return 0;
        return count($this->eventConnectionsDic[$eventName]);
    }

    /**
     * 获取指定事件的所有订阅者
     * @param string $eventName
     * @return TcpConnection[]
     */
    public function getEventSubscribers(string $eventName): array
    {
        if (!array_key_exists($eventName, $this->eventConnectionsDic)) return [];
        return $this->eventConnectionsDic[$eventName];
    }
}